package message;

import java.math.BigInteger;
import java.util.LinkedList;

import crypto.Keys;

import util.Algorithms;
import util.Domain;
import util.Point;

/**
 * 
 * @author Ralf
 *
 */
public class Conversion {
	
	private static final BigInteger prime = new BigInteger("26893");
	private static final BigInteger modulus = new BigInteger("2000");
	
	public static Cipher messageToCipher(String message, Keys keyPair, Point receiverKey, BigInteger k) {
		// try to convert the whole message with the given parameters
		LinkedList<Point> points = new LinkedList<Point>();
		//prime = Algorithms.getNextLargePrime();
		// it throws an exception, if it did not succeed
		boolean success = false;
		do {
			try {
				points = messageToPoints(message, keyPair.getDomain(), k);
				success = true;
			} catch (IllegalArgumentException e) {
				success = false;
				k = k.add(BigInteger.TEN);
			}
		} while (!success);
		
		Point publicKey = keyPair.getPublicKey();
		LinkedList<Point> encryptedPoints = new LinkedList<Point>();
		for (int i = 0; i < points.size(); i++) {
			encryptedPoints.add(keyPair.encryptPoint(points.get(i), receiverKey));
		}
		
		return new Cipher(publicKey, k, encryptedPoints);
	}
	
	private static LinkedList<Point> messageToPoints(String message, Domain d, BigInteger k) {
		LinkedList<Point> points = new LinkedList<Point>();
		// try to convert the whole message at once
		Point plaintextPoint = embed(stringToNumber(message), d, prime, k);
		
		if (!plaintextPoint.isPointOfInfinity()) {
			assert(d.getCurve().isOnCurve(plaintextPoint));
			points.add(plaintextPoint);
		} else {
			if (message.length() == 1) {
				// message is already only one symbol long
				throw new IllegalArgumentException("m cannot be split any further");
			} else {
				// message can be split into smaller parts
				String part1 = message.substring(0, message.length() / 2);
				String part2 = message.substring(message.length() / 2, message.length());
				
				points.addAll(messageToPoints(part1, d, k));
				points.addAll(messageToPoints(part2, d, k));				
			}
		}
		return points;
	}
	
	public static void signCipher(Cipher cipher, Keys keyPair) {
		if (cipher.isSigned()) {
			throw new IllegalArgumentException("Cipher is already signed.");
		}
		
		LinkedList<Point> points = cipher.getEncryptedPoints();
		LinkedList<Point> signatures = new LinkedList<Point>();
		for (int i = 0; i < points.size(); i++) {
			signatures.add(keyPair.signPoint(points.get(i)));
		}
		cipher.addSignatures(signatures);
	}
	
	public static String cipherToMessage(Cipher cipher, Keys keyPair) {
		String message = new String();
		LinkedList<Point> encryptedPoints = cipher.getEncryptedPoints();
		for (int i = 0; i < encryptedPoints.size(); i++) {
			Point decryptedPoint = keyPair.decryptPoint(encryptedPoints.get(i), cipher.getPublicKey());
			message += numberToString(decode(decryptedPoint, cipher.getK()));
		}
		return message;
	}
	
	public static boolean verifyCipher(Cipher cipher, Keys keyPair) {
		if (!cipher.isSigned()) {
			throw new IllegalArgumentException("Cipher is not signed.");
		}
		
		LinkedList<Point> encryptedPoints = cipher.getEncryptedPoints();
		LinkedList<Point> signatures = cipher.getSignatures();
		
		for (int i = 0; i < encryptedPoints.size(); i++) {
			if (!keyPair.verifySignature(encryptedPoints.get(i), cipher.getPublicKey(), signatures.get(i))) {
				return false;
			}
		}
		return true;
	}
	
	public static boolean verifyPublicKeyOwner(Cipher cipher, Point publicKey) {
		return cipher.getPublicKey().isEqual(publicKey);
	}
	
	
	/**
	 * This method converses a message in form of a BigInteger into a point on the curve provided
	 * by the domain
	 * @param message the message to embed in form of a BigInteger
	 * @param d the domain used for embedding the message
	 * @param prime a prime number used for this algorithm
	 * @param k 
	 * @return a valid point if embedding was successful, a Point of Infinity otherwise
	 */
	private static Point embed(BigInteger message, Domain d, BigInteger prime, BigInteger k) {

		// check if m < (p-k)/k
		// => p > m*k + k
		// return, if not
//		BigInteger val = message.multiply(k).add(k);
//		if (val.compareTo(prime) >= 0) {
//			return new Point();
//		}
		
//		// need a prime number p > m*k + k
//		BigInteger primeNumber = Algorithms.getLargePrime(message.multiply(k).add(k));
//		prime = primeNumber;
//		if (primeNumber.equals(BigInteger.ZERO)) {
//			System.out.println("Not large enough.");
//			return new Point();
//		}
//		//
//		System.out.println(prime.toString(16));
		
		BigInteger remainder = prime.mod(modulus);
		BigInteger delta = modulus.subtract(remainder);
		
		BigInteger xj, yj, x3, ax, tmp;
		for(BigInteger j = BigInteger.ZERO; j.compareTo(k) < 0; j = j.add(BigInteger.ONE)) {
			xj = message.multiply(k).add(j);
			x3 = xj.pow(3);
			ax = xj.multiply(d.getCurve().getA());
			tmp = x3.add(ax).add(d.getCurve().getB());
			yj = Algorithms.pow(tmp, prime.add(delta).divide(modulus));
			if (yj.pow(2).mod(prime).compareTo(tmp.mod(prime)) == 0) {
				return new Point(xj, yj);
			}
		}

		return new Point();
	}
	
	/**
	 * Extracts the message in form of a BigInteger out of a point
	 * @param messagePoint the point containing the message
	 * @param k  
	 * @return the decoded message in form of a BigInteger, if the cipher is a valid point. 
	 */
	private static BigInteger decode(Point messagePoint, BigInteger k) {
		if (!messagePoint.isPointOfInfinity()) {
			return messagePoint.getX().divide(k);
		}
		return BigInteger.ZERO;
	}
	
	/**
	 * Converts a String into a BigInteger
	 * @param message a String to be converted
	 * @return the BigInteger representation of the String
	 */
	private static BigInteger stringToNumber(String message) {
		//System.out.println("BigInt: " + (new BigInteger(message.getBytes())).toString());
		return new BigInteger(message.getBytes());
	}
	
	/**
	 * Converts a BigInteger into a String.
	 * This is not a toString-method!
	 * @param number a BigInteger to be converted
	 * @return the String, which represents the BigInteger-value
	 */
	private static String numberToString(BigInteger number) {
		//System.out.println("number: "+ number.toString());
		return new String(number.toByteArray());
	}

}
